Skip to content

NO-JIRA: test(e2e-v2): add ginkgo-based v2 test suite - #7152

Merged
openshift-merge-bot[bot] merged 2 commits into
openshift:feat-e2e-v2from
csrwng:v2-tests
Nov 7, 2025
Merged

NO-JIRA: test(e2e-v2): add ginkgo-based v2 test suite#7152
openshift-merge-bot[bot] merged 2 commits into
openshift:feat-e2e-v2from
csrwng:v2-tests

Conversation

@csrwng

@csrwng csrwng commented Nov 3, 2025

Copy link
Copy Markdown
Contributor

Introduce a new Ginkgo v2 based end-to-end test suite with improved
structure and organization. The new suite includes:

  • Suite setup with BeforeSuite/AfterSuite hooks for test initialization
  • Test organization under test/e2e/v2/tests/ directory
  • Internal utilities and helpers in test/e2e/v2/internal/
  • CRD utility functions in test/e2e/util/ for cross-version compatibility

This new test framework will enable more maintainable and scalable e2e
testing with better parallel execution support and clearer test structure.

Signed-off-by: Cesar Wong cewong@redhat.com
Assisted-by: Claude 3.7 Sonnet (via Claude Code)

Add Ginkgo v2 testing framework and update related vendored dependencies
including gomega matchers, protobuf, and supporting libraries needed for
the new e2e v2 test suite.

Signed-off-by: Cesar Wong <cewong@redhat.com>
Assisted-by: Claude 3.7 Sonnet (via Claude Code)
@openshift-ci

openshift-ci Bot commented Nov 3, 2025

Copy link
Copy Markdown
Contributor

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@openshift-ci openshift-ci Bot added do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. do-not-merge/needs-area labels Nov 3, 2025
@coderabbitai

coderabbitai Bot commented Nov 3, 2025

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Adds a v2 E2E testing framework: dependency updates (Ginkgo v2/Gomega), environment registry, test context, workload registry/resolver, CRD schema helper, version helpers, extensive API UX and control-plane workload tests, and YAML test assets.

Changes

Cohort / File(s) Summary
Dependency Management
go.mod
Bumps/introduces test and tooling deps: github.com/onsi/ginkgo/v2, github.com/onsi/gomega v1.38.2, google.golang.org/protobuf v1.36.7, golang.org/x/net v0.43.0, plus several indirect module updates.
CRD Schema Utility
test/e2e/util/crd.go
New HasFieldInCRDSchema(ctx, client, crdName, fieldPath) (bool, error) that fetches a CRD and recursively inspects its OpenAPI v3 JSONSchemaProps (handles properties, allOf/anyOf/oneOf) to detect a dotted field path.
Version helpers & Ginkgo integration
test/e2e/util/version.go
Adds SetReleaseVersionFromHostedCluster(ctx, hostedCluster) to derive/normalize release version and GinkgoAtLeast(version semver.Version) to skip tests based on releaseVersion using Ginkgo.
E2E v2 env var registry
test/e2e/v2/internal/env_vars.go
New registry for environment variables: EnvVarSpec, RegisterEnvVar*, GetEnvVarValue, PrintEnvVarHelp, masking of sensitive values, and pre-registered e2e vars (E2E_HOSTED_CLUSTER_*, E2E_SHOW_ENV_HELP, E2E_STRICT_MODE).
Test context infra
test/e2e/v2/internal/test_context.go
New TestContext type, lazy HostedCluster accessor, global setter/getter, SetupTestContext and SetupTestContextFromEnv to initialize context (including MgmtClient and control plane namespace); includes strict mode parsing.
Workload registry
test/e2e/v2/internal/workload_registry.go
Generated registry WorkloadSpec and GetControlPlaneWorkloads() returning static list of control-plane workload specs (type, name, pod selector, optional platform).
Workload resolver & helpers
test/e2e/v2/internal/workload_resolver.go
Adds WorkloadInfo and functions: ListWorkloads, GetWorkloadPods, ResolveWorkloadForPod, GetWorkloadPodsBySelector; owner-reference traversal with cycle protection, deterministic sorting, and special-case handling for Jobs/CronJobs.
Test suite entry & bootstrap
test/e2e/v2/tests/suite_test.go
New test entrypoints TestShowEnvHelp, TestE2EV2 and BeforeSuite that prints env help optionally, initializes TestContext from env and sets global context; integrates Ginkgo v2 test runner.
API UX validation tests
test/e2e/v2/tests/api_ux_validation_test.go
New comprehensive tests validating HostedCluster and NodePool CRD field constraints and cross-field rules, with helper logic to mutate/load base YAML assets and assert validation errors.
Control-plane workload tests
test/e2e/v2/tests/control_plane_workloads_test.go
New extensive control-plane tests covering many per-workload checks (security, resources, annotations, affinities/tolerations, platform gating, strict-mode validation) and registration wiring for many per-workload test cases.
Test assets
test/e2e/v2/tests/assets/...
hostedcluster-base.yaml, nodepool-base.yaml, karpenter-nodepool.yaml, karpenter-workloads.yaml
Adds YAML fixtures for HostedCluster, NodePool, Karpenter NodePool and workloads used by tests (base templates and deployment manifest).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Focus review on:
    • test/e2e/v2/internal/workload_resolver.go — owner-reference traversal, cycle protection, Job vs CronJob handling.
    • test/e2e/v2/tests/api_ux_validation_test.go and control_plane_workloads_test.go — correctness of test assertions, mutation helpers, cleanup, and platform/exemption logic.
    • test/e2e/v2/internal/test_context.go and env_vars.go — initialization patterns, panic vs error behavior, and environment-driven strict mode.
    • test/e2e/util/crd.go — correctness of schema traversal across AllOf/AnyOf/OneOf and selection of served schema version.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

@openshift-ci openshift-ci Bot added the area/testing Indicates the PR includes changes for e2e testing label Nov 3, 2025
@openshift-ci

openshift-ci Bot commented Nov 3, 2025

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: csrwng

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci openshift-ci Bot added approved Indicates a PR has been approved by an approver from all required OWNERS files. and removed do-not-merge/needs-area labels Nov 3, 2025
@csrwng

csrwng commented Nov 4, 2025

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Nov 4, 2025

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between e51f3c5 and d8333bd.

⛔ Files ignored due to path filters (269)
  • go.sum is excluded by !**/*.sum
  • vendor/github.com/Masterminds/semver/v3/.gitignore is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/Masterminds/semver/v3/.golangci.yml is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/Masterminds/semver/v3/CHANGELOG.md is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/Masterminds/semver/v3/LICENSE.txt is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/Masterminds/semver/v3/Makefile is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/Masterminds/semver/v3/README.md is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/Masterminds/semver/v3/SECURITY.md is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/Masterminds/semver/v3/collection.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/Masterminds/semver/v3/constraints.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/Masterminds/semver/v3/doc.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/Masterminds/semver/v3/version.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/go-task/slim-sprig/v3/.editorconfig is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/go-task/slim-sprig/v3/.gitattributes is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/go-task/slim-sprig/v3/.gitignore is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/go-task/slim-sprig/v3/CHANGELOG.md is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/go-task/slim-sprig/v3/LICENSE.txt is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/go-task/slim-sprig/v3/README.md is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/go-task/slim-sprig/v3/Taskfile.yml is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/go-task/slim-sprig/v3/crypto.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/go-task/slim-sprig/v3/date.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/go-task/slim-sprig/v3/defaults.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/go-task/slim-sprig/v3/dict.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/go-task/slim-sprig/v3/doc.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/go-task/slim-sprig/v3/functions.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/go-task/slim-sprig/v3/list.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/go-task/slim-sprig/v3/network.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/go-task/slim-sprig/v3/numeric.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/go-task/slim-sprig/v3/reflect.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/go-task/slim-sprig/v3/regex.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/go-task/slim-sprig/v3/strings.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/go-task/slim-sprig/v3/url.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/.gitignore is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/CHANGELOG.md is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/CONTRIBUTING.md is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/LICENSE is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/Makefile is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/README.md is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/RELEASING.md is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/config/deprecated.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/core_dsl.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/decorator_dsl.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/deprecated_dsl.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/formatter/colorable_others.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/formatter/colorable_windows.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/formatter/formatter.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/automaxprocs.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/automaxprocs/README.md is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/automaxprocs/automaxprocs.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/automaxprocs/cgroup.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/automaxprocs/cgroups.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/automaxprocs/cgroups2.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/automaxprocs/cpu_quota_linux.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/automaxprocs/cpu_quota_unsupported.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/automaxprocs/errors.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/automaxprocs/mountpoint.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/automaxprocs/runtime.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/automaxprocs/subsys.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/build/build_command.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/command/abort.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/command/command.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/command/program.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/generators/boostrap_templates.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/generators/bootstrap_command.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/generators/generate_command.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/generators/generate_templates.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/generators/generators_common.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/internal/compile.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/internal/gocovmerge.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/internal/profiles_and_reports.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/internal/run.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/internal/test_suite.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/internal/utils.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/internal/verify_version.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/labels/labels_command.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/main.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/outline/ginkgo.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/outline/import.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/outline/outline.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/outline/outline_command.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/run/run_command.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/unfocus/unfocus_command.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/watch/delta.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/watch/delta_tracker.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/watch/dependencies.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/watch/package_hash.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/watch/package_hashes.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/watch/suite.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/watch/watch_command.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo_cli_dependencies.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo_t_dsl.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/around_node.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/counter.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/failer.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/focus.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/global/init.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/group.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/interrupt_handler/interrupt_handler.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/interrupt_handler/sigquit_swallower_unix.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/interrupt_handler/sigquit_swallower_windows.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/node.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/ordering.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/output_interceptor.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/output_interceptor_unix.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/output_interceptor_wasm.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/output_interceptor_win.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/parallel_support/client_server.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/parallel_support/http_client.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/parallel_support/http_server.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/parallel_support/rpc_client.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/parallel_support/rpc_server.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/parallel_support/server_handler.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/progress_report.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/progress_report_bsd.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/progress_report_unix.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/progress_report_wasm.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/progress_report_win.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/progress_reporter_manager.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/report_entry.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/reporters/gojson.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/reporters/gojson_event_writer.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/reporters/gojson_reporter.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/spec.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/spec_context.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/suite.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/testingtproxy/testing_t_proxy.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/tree.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/writer.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/reporters/default_reporter.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/reporters/deprecated_reporter.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/reporters/gojson_report.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/reporters/json_report.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/reporters/junit_report.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/reporters/reporter.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/reporters/teamcity_report.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/reporting_dsl.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/table_dsl.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/types/around_node.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/types/code_location.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/types/config.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/types/deprecated_types.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/types/deprecation_support.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/types/enum_support.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/types/errors.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/types/file_filter.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/types/flags.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/types/label_filter.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/types/report_entry.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/types/semver_filter.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/types/types.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/types/version.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/gomega/CHANGELOG.md is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/gomega/gomega_dsl.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/gomega/internal/async_assertion.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/gomega/matchers/be_comparable_to_matcher.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/gomega/matchers/match_yaml_matcher.go is excluded by !vendor/**, !**/vendor/**
  • vendor/go.yaml.in/yaml/v3/LICENSE is excluded by !vendor/**, !**/vendor/**
  • vendor/go.yaml.in/yaml/v3/NOTICE is excluded by !vendor/**, !**/vendor/**
  • vendor/go.yaml.in/yaml/v3/README.md is excluded by !vendor/**, !**/vendor/**
  • vendor/go.yaml.in/yaml/v3/apic.go is excluded by !vendor/**, !**/vendor/**
  • vendor/go.yaml.in/yaml/v3/decode.go is excluded by !vendor/**, !**/vendor/**
  • vendor/go.yaml.in/yaml/v3/emitterc.go is excluded by !vendor/**, !**/vendor/**
  • vendor/go.yaml.in/yaml/v3/encode.go is excluded by !vendor/**, !**/vendor/**
  • vendor/go.yaml.in/yaml/v3/parserc.go is excluded by !vendor/**, !**/vendor/**
  • vendor/go.yaml.in/yaml/v3/readerc.go is excluded by !vendor/**, !**/vendor/**
  • vendor/go.yaml.in/yaml/v3/resolve.go is excluded by !vendor/**, !**/vendor/**
  • vendor/go.yaml.in/yaml/v3/scannerc.go is excluded by !vendor/**, !**/vendor/**
  • vendor/go.yaml.in/yaml/v3/sorter.go is excluded by !vendor/**, !**/vendor/**
  • vendor/go.yaml.in/yaml/v3/writerc.go is excluded by !vendor/**, !**/vendor/**
  • vendor/go.yaml.in/yaml/v3/yaml.go is excluded by !vendor/**, !**/vendor/**
  • vendor/go.yaml.in/yaml/v3/yamlh.go is excluded by !vendor/**, !**/vendor/**
  • vendor/go.yaml.in/yaml/v3/yamlprivateh.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/net/http2/http2.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/LICENSE is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/PATENTS is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/cover/profile.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/go/ast/edge/edge.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/go/ast/inspector/cursor.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/go/ast/inspector/inspector.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/go/ast/inspector/iter.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/go/ast/inspector/typeof.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/go/ast/inspector/walk.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/go/gcexportdata/gcexportdata.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/go/gcexportdata/importer.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/go/packages/doc.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/go/packages/external.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/go/packages/golist.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/go/packages/golist_overlay.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/go/packages/loadmode_string.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/go/packages/packages.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/go/packages/visit.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/go/types/objectpath/objectpath.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/go/types/typeutil/callee.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/go/types/typeutil/imports.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/go/types/typeutil/map.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/go/types/typeutil/methodsetcache.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/go/types/typeutil/ui.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/aliases/aliases.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/aliases/aliases_go122.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/event/core/event.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/event/core/export.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/event/core/fast.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/event/doc.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/event/event.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/event/keys/keys.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/event/keys/standard.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/event/keys/util.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/event/label/label.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/gcimporter/bimport.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/gcimporter/exportdata.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/gcimporter/gcimporter.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/gcimporter/iexport.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/gcimporter/iimport.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/gcimporter/iimport_go122.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/gcimporter/predeclared.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/gcimporter/support.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/gcimporter/ureader_yes.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/gocommand/invoke.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/gocommand/invoke_notunix.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/gocommand/invoke_unix.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/gocommand/vendor.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/gocommand/version.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/packagesinternal/packages.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/pkgbits/codes.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/pkgbits/decoder.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/pkgbits/doc.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/pkgbits/encoder.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/pkgbits/flags.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/pkgbits/reloc.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/pkgbits/support.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/pkgbits/sync.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/pkgbits/syncmarker_string.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/pkgbits/version.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/stdlib/deps.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/stdlib/import.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/stdlib/manifest.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/stdlib/stdlib.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/typeparams/common.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/typeparams/coretype.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/typeparams/free.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/typeparams/normalize.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/typeparams/termlist.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/typeparams/typeterm.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/typesinternal/classify_call.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/typesinternal/element.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/typesinternal/errorcode.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/typesinternal/errorcode_string.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/typesinternal/qualifier.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/typesinternal/recv.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/typesinternal/toonew.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/typesinternal/types.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/typesinternal/varkind.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/typesinternal/zerovalue.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/versions/features.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/versions/gover.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/versions/types.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/versions/versions.go is excluded by !vendor/**, !**/vendor/**
  • vendor/google.golang.org/protobuf/encoding/protowire/wire.go is excluded by !vendor/**, !**/vendor/**
  • vendor/google.golang.org/protobuf/internal/editiondefaults/editions_defaults.binpb is excluded by !vendor/**, !**/vendor/**
  • vendor/google.golang.org/protobuf/internal/filedesc/editions.go is excluded by !vendor/**, !**/vendor/**
  • vendor/google.golang.org/protobuf/internal/filedesc/presence.go is excluded by !vendor/**, !**/vendor/**
  • vendor/google.golang.org/protobuf/internal/genid/descriptor_gen.go is excluded by !vendor/**, !**/vendor/**
  • vendor/google.golang.org/protobuf/internal/impl/codec_message_opaque.go is excluded by !vendor/**, !**/vendor/**
  • vendor/google.golang.org/protobuf/internal/impl/message_opaque.go is excluded by !vendor/**, !**/vendor/**
  • vendor/google.golang.org/protobuf/internal/impl/presence.go is excluded by !vendor/**, !**/vendor/**
  • vendor/google.golang.org/protobuf/internal/version/version.go is excluded by !vendor/**, !**/vendor/**
  • vendor/google.golang.org/protobuf/reflect/protoreflect/source_gen.go is excluded by !vendor/**, !**/vendor/**
  • vendor/google.golang.org/protobuf/types/descriptorpb/descriptor.pb.go is excluded by !**/*.pb.go, !vendor/**, !**/vendor/**
  • vendor/modules.txt is excluded by !vendor/**, !**/vendor/**
📒 Files selected for processing (14)
  • go.mod (5 hunks)
  • test/e2e/util/crd.go (1 hunks)
  • test/e2e/util/version.go (2 hunks)
  • test/e2e/v2/internal/env_vars.go (1 hunks)
  • test/e2e/v2/internal/test_context.go (1 hunks)
  • test/e2e/v2/internal/workload_registry.go (1 hunks)
  • test/e2e/v2/internal/workload_resolver.go (1 hunks)
  • test/e2e/v2/tests/api_ux_validation_test.go (1 hunks)
  • test/e2e/v2/tests/assets/hostedcluster-base.yaml (1 hunks)
  • test/e2e/v2/tests/assets/karpenter-nodepool.yaml (1 hunks)
  • test/e2e/v2/tests/assets/karpenter-workloads.yaml (1 hunks)
  • test/e2e/v2/tests/assets/nodepool-base.yaml (1 hunks)
  • test/e2e/v2/tests/control_plane_workloads_test.go (1 hunks)
  • test/e2e/v2/tests/suite_test.go (1 hunks)

Comment thread test/e2e/util/crd.go
Comment on lines +41 to +80
func hasFieldInSchema(schema *apiextensionsv1.JSONSchemaProps, pathParts []string, index int) bool {
if schema == nil || index >= len(pathParts) {
return index == len(pathParts)
}

currentPart := pathParts[index]

// Check properties first
if schema.Properties != nil {
if prop, exists := schema.Properties[currentPart]; exists {
if index == len(pathParts)-1 {
// This is the last part, field exists
return true
}
// Recurse into the property
return hasFieldInSchema(&prop, pathParts, index+1)
}
}

// Check AllOf, AnyOf, OneOf - these can contain the field
for i := range schema.AllOf {
if hasFieldInSchema(&schema.AllOf[i], pathParts, index) {
return true
}
}
for i := range schema.AnyOf {
if hasFieldInSchema(&schema.AnyOf[i], pathParts, index) {
return true
}
}
for i := range schema.OneOf {
if hasFieldInSchema(&schema.OneOf[i], pathParts, index) {
return true
}
}

// Check if there's a $ref that we need to follow
// Note: In CRDs, $ref typically points to definitions within the same schema
// For simplicity, we check properties which is the most common case
return false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Handle array members when traversing schemas. Today any path that dives into an array (for example status.conditions.type) is reported missing because we never follow schema.Items. That causes false negatives in the new helpers and will break tests that rely on legitimate CRD fields living inside arrays. Please descend into Items without advancing the path index, e.g.:

@@
-	// Check AllOf, AnyOf, OneOf - these can contain the field
+	// Bridge into array items before falling back to the combinators.
+	if schema.Items != nil {
+		if schema.Items.Schema != nil && hasFieldInSchema(schema.Items.Schema, pathParts, index) {
+			return true
+		}
+		for i := range schema.Items.JSONSchemas {
+			if hasFieldInSchema(&schema.Items.JSONSchemas[i], pathParts, index) {
+				return true
+			}
+		}
+	}
+
+	// Check AllOf, AnyOf, OneOf - these can contain the field
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func hasFieldInSchema(schema *apiextensionsv1.JSONSchemaProps, pathParts []string, index int) bool {
if schema == nil || index >= len(pathParts) {
return index == len(pathParts)
}
currentPart := pathParts[index]
// Check properties first
if schema.Properties != nil {
if prop, exists := schema.Properties[currentPart]; exists {
if index == len(pathParts)-1 {
// This is the last part, field exists
return true
}
// Recurse into the property
return hasFieldInSchema(&prop, pathParts, index+1)
}
}
// Check AllOf, AnyOf, OneOf - these can contain the field
for i := range schema.AllOf {
if hasFieldInSchema(&schema.AllOf[i], pathParts, index) {
return true
}
}
for i := range schema.AnyOf {
if hasFieldInSchema(&schema.AnyOf[i], pathParts, index) {
return true
}
}
for i := range schema.OneOf {
if hasFieldInSchema(&schema.OneOf[i], pathParts, index) {
return true
}
}
// Check if there's a $ref that we need to follow
// Note: In CRDs, $ref typically points to definitions within the same schema
// For simplicity, we check properties which is the most common case
return false
func hasFieldInSchema(schema *apiextensionsv1.JSONSchemaProps, pathParts []string, index int) bool {
if schema == nil || index >= len(pathParts) {
return index == len(pathParts)
}
currentPart := pathParts[index]
// Check properties first
if schema.Properties != nil {
if prop, exists := schema.Properties[currentPart]; exists {
if index == len(pathParts)-1 {
// This is the last part, field exists
return true
}
// Recurse into the property
return hasFieldInSchema(&prop, pathParts, index+1)
}
}
// Bridge into array items before falling back to the combinators.
if schema.Items != nil {
if schema.Items.Schema != nil && hasFieldInSchema(schema.Items.Schema, pathParts, index) {
return true
}
for i := range schema.Items.JSONSchemas {
if hasFieldInSchema(&schema.Items.JSONSchemas[i], pathParts, index) {
return true
}
}
}
// Check AllOf, AnyOf, OneOf - these can contain the field
for i := range schema.AllOf {
if hasFieldInSchema(&schema.AllOf[i], pathParts, index) {
return true
}
}
for i := range schema.AnyOf {
if hasFieldInSchema(&schema.AnyOf[i], pathParts, index) {
return true
}
}
for i := range schema.OneOf {
if hasFieldInSchema(&schema.OneOf[i], pathParts, index) {
return true
}
}
// Check if there's a $ref that we need to follow
// Note: In CRDs, $ref typically points to definitions within the same schema
// For simplicity, we check properties which is the most common case
return false
🤖 Prompt for AI Agents
In test/e2e/util/crd.go around lines 41 to 80, the schema traversal currently
ignores array members so any path that descends into an array (e.g.
status.conditions.type) is reported missing; update the function to descend into
schema.Items without advancing the path index: if schema.Items != nil then if
schema.Items.Schema != nil call hasFieldInSchema(schema.Items.Schema, pathParts,
index) and return true if it matches; also handle the case where
schema.Items.JSONSchemas (array form) exists by iterating those entries and
calling hasFieldInSchema on each with the same index; return true if any match.
Ensure you do not increment index when recursing into Items.

Comment thread test/e2e/util/version.go
Comment on lines +64 to +77
func SetReleaseVersionFromHostedCluster(ctx context.Context, hostedCluster *hyperv1.HostedCluster) error {
if len(hostedCluster.Status.Version.History) == 0 {
return fmt.Errorf("hostedCluster has no version history")
}
hcVersion := hostedCluster.Status.Version.History[0].Version
var err error
releaseVersion, err = semver.Parse(hcVersion)
if err != nil {
return fmt.Errorf("error parsing version: %v", err)
}
releaseVersion.Patch = 0
releaseVersion.Pre = nil
releaseVersion.Build = nil
return nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Guard against nil HostedCluster status version

SetReleaseVersionFromHostedCluster dereferences hostedCluster.Status.Version.History without checking whether Status.Version is nil. When the status hasn't been populated yet (common early in cluster lifecycle), this will panic before we can return an error. Please add an explicit nil check (and bail early with an error) before touching the history slice.

 func SetReleaseVersionFromHostedCluster(ctx context.Context, hostedCluster *hyperv1.HostedCluster) error {
-	if len(hostedCluster.Status.Version.History) == 0 {
+	if hostedCluster.Status.Version == nil {
+		return fmt.Errorf("hostedCluster has no version status")
+	}
+	if len(hostedCluster.Status.Version.History) == 0 {
 		return fmt.Errorf("hostedCluster has no version history")
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func SetReleaseVersionFromHostedCluster(ctx context.Context, hostedCluster *hyperv1.HostedCluster) error {
if len(hostedCluster.Status.Version.History) == 0 {
return fmt.Errorf("hostedCluster has no version history")
}
hcVersion := hostedCluster.Status.Version.History[0].Version
var err error
releaseVersion, err = semver.Parse(hcVersion)
if err != nil {
return fmt.Errorf("error parsing version: %v", err)
}
releaseVersion.Patch = 0
releaseVersion.Pre = nil
releaseVersion.Build = nil
return nil
func SetReleaseVersionFromHostedCluster(ctx context.Context, hostedCluster *hyperv1.HostedCluster) error {
if hostedCluster.Status.Version == nil {
return fmt.Errorf("hostedCluster has no version status")
}
if len(hostedCluster.Status.Version.History) == 0 {
return fmt.Errorf("hostedCluster has no version history")
}
hcVersion := hostedCluster.Status.Version.History[0].Version
var err error
releaseVersion, err = semver.Parse(hcVersion)
if err != nil {
return fmt.Errorf("error parsing version: %v", err)
}
releaseVersion.Patch = 0
releaseVersion.Pre = nil
releaseVersion.Build = nil
return nil
🤖 Prompt for AI Agents
In test/e2e/util/version.go around lines 64 to 77, the function dereferences
hostedCluster.Status.Version.History without checking for nil; add explicit nil
checks for hostedCluster.Status and hostedCluster.Status.Version (and optionally
hostedCluster.Status.Version.History) before accessing History and return a
descriptive error if any are nil to avoid a panic. Update the beginning of the
function to validate hostedCluster.Status != nil and
hostedCluster.Status.Version != nil (and that History exists/has length) and
bail out with fmt.Errorf(...) if the checks fail, leaving the rest of the
parsing logic unchanged.

Comment on lines +168 to +199
if owner.Kind == workload.Kind && owner.Name == workload.Name && ownerUID == workload.UID {
return true
}

// If we found a top-level workload type (Deployment, StatefulSet, CronJob, standalone Job), stop
if owner.Kind == "Deployment" || owner.Kind == "StatefulSet" || owner.Kind == "CronJob" || owner.Kind == "Job" {
break
}

// Follow the chain for ReplicaSet -> Deployment or Job -> CronJob
var nextOwner *metav1.OwnerReference
switch owner.Kind {
case "ReplicaSet":
rs := &appsv1.ReplicaSet{}
if err := client.Get(ctx, types.NamespacedName{Namespace: pod.Namespace, Name: owner.Name}, rs); err != nil {
break
}
if len(rs.OwnerReferences) > 0 && rs.OwnerReferences[0].Kind == "Deployment" {
nextOwner = &rs.OwnerReferences[0]
}
case "Job":
job := &batchv1.Job{}
if err := client.Get(ctx, types.NamespacedName{Namespace: pod.Namespace, Name: owner.Name}, job); err != nil {
break
}
if len(job.OwnerReferences) > 0 && job.OwnerReferences[0].Kind == "CronJob" {
nextOwner = &job.OwnerReferences[0]
}
default:
break
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Allow traversal from Jobs up to CronJobs.

belongsToWorkload breaks out as soon as it sees an owner of kind Job, so CronJob-backed pods never match their workload and all CronJob tests silently skip. Remove Job from the early break so the subsequent traversal can reach and verify the CronJob owner.

-		// If we found a top-level workload type (Deployment, StatefulSet, CronJob, standalone Job), stop
-		if owner.Kind == "Deployment" || owner.Kind == "StatefulSet" || owner.Kind == "CronJob" || owner.Kind == "Job" {
+		// If we found a top-level workload type (Deployment, StatefulSet, CronJob), stop
+		if owner.Kind == "Deployment" || owner.Kind == "StatefulSet" || owner.Kind == "CronJob" {
 			break
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if owner.Kind == workload.Kind && owner.Name == workload.Name && ownerUID == workload.UID {
return true
}
// If we found a top-level workload type (Deployment, StatefulSet, CronJob, standalone Job), stop
if owner.Kind == "Deployment" || owner.Kind == "StatefulSet" || owner.Kind == "CronJob" || owner.Kind == "Job" {
break
}
// Follow the chain for ReplicaSet -> Deployment or Job -> CronJob
var nextOwner *metav1.OwnerReference
switch owner.Kind {
case "ReplicaSet":
rs := &appsv1.ReplicaSet{}
if err := client.Get(ctx, types.NamespacedName{Namespace: pod.Namespace, Name: owner.Name}, rs); err != nil {
break
}
if len(rs.OwnerReferences) > 0 && rs.OwnerReferences[0].Kind == "Deployment" {
nextOwner = &rs.OwnerReferences[0]
}
case "Job":
job := &batchv1.Job{}
if err := client.Get(ctx, types.NamespacedName{Namespace: pod.Namespace, Name: owner.Name}, job); err != nil {
break
}
if len(job.OwnerReferences) > 0 && job.OwnerReferences[0].Kind == "CronJob" {
nextOwner = &job.OwnerReferences[0]
}
default:
break
}
if owner.Kind == workload.Kind && owner.Name == workload.Name && ownerUID == workload.UID {
return true
}
// If we found a top-level workload type (Deployment, StatefulSet, CronJob), stop
if owner.Kind == "Deployment" || owner.Kind == "StatefulSet" || owner.Kind == "CronJob" {
break
}
// Follow the chain for ReplicaSet -> Deployment or Job -> CronJob
var nextOwner *metav1.OwnerReference
switch owner.Kind {
case "ReplicaSet":
rs := &appsv1.ReplicaSet{}
if err := client.Get(ctx, types.NamespacedName{Namespace: pod.Namespace, Name: owner.Name}, rs); err != nil {
break
}
if len(rs.OwnerReferences) > 0 && rs.OwnerReferences[0].Kind == "Deployment" {
nextOwner = &rs.OwnerReferences[0]
}
case "Job":
job := &batchv1.Job{}
if err := client.Get(ctx, types.NamespacedName{Namespace: pod.Namespace, Name: owner.Name}, job); err != nil {
break
}
if len(job.OwnerReferences) > 0 && job.OwnerReferences[0].Kind == "CronJob" {
nextOwner = &job.OwnerReferences[0]
}
default:
break
}
🤖 Prompt for AI Agents
In test/e2e/v2/internal/workload_resolver.go around lines 168 to 199, the early
exit checks include "Job" which prevents traversal from Job -> CronJob and
causes CronJob-backed pods to be missed; remove "Job" from the top-level break
condition (leave Deployment, StatefulSet, CronJob only) so the switch that
follows can resolve Job owners up to CronJob, keeping the existing ReplicaSet
and Job lookup logic intact.

Comment on lines +1311 to +1317
It("should reject when image is empty", func() {
err := testNodePoolCreation(ctx, mgmtClient, "nodepool-base.yaml", func(np *hyperv1.NodePool) {
np.Spec.Release.Image = "@"
})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("Image must start with a word character (letters, digits, or underscores) and contain no white spaces"))
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix “image is empty” mutation

This case is meant to cover an empty release image, but it still sets the image to "@", duplicating the preceding “bad format” test. As written, we never assert the empty-string path, so a regression there would slip through.

-					np.Spec.Release.Image = "@"
+					np.Spec.Release.Image = ""
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
It("should reject when image is empty", func() {
err := testNodePoolCreation(ctx, mgmtClient, "nodepool-base.yaml", func(np *hyperv1.NodePool) {
np.Spec.Release.Image = "@"
})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("Image must start with a word character (letters, digits, or underscores) and contain no white spaces"))
})
It("should reject when image is empty", func() {
err := testNodePoolCreation(ctx, mgmtClient, "nodepool-base.yaml", func(np *hyperv1.NodePool) {
np.Spec.Release.Image = ""
})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("Image must start with a word character (letters, digits, or underscores) and contain no white spaces"))
})
🤖 Prompt for AI Agents
In test/e2e/v2/tests/api_ux_validation_test.go around lines 1311 to 1317, the
test intended to validate an empty release image mistakenly sets
np.Spec.Release.Image = "@" (duplicating the bad-format case); change the
mutation to set np.Spec.Release.Image = "" to exercise the empty-string path,
and update the Expect assertion to check for the empty-image error message
(e.g., assert the error mentions that the image must not be empty or similar
project-specific empty-field text).

Comment on lines +29 to +49
services:
- service: APIServer
servicePublishingStrategy:
type: Route
route:
hostname: api.agarcial.hypershift.devcluster.openshift.com
- service: OAuthServer
servicePublishingStrategy:
type: Route
route:
hostname: OAuthServer.agarcial.hypershift.devcluster.openshift.com
- service: Konnectivity
servicePublishingStrategy:
type: Route
route:
hostname: Konnectivity.agarcial.hypershift.devcluster.openshift.com
- service: Ignition
servicePublishingStrategy:
type: Route
route:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix invalid route hostnames. The OAuthServer and Konnectivity routes currently use capital letters, and Ignition leaves hostname empty. DNS-1123 hostnames must be lowercase and non-empty, otherwise the API will reject the manifest before the suite ever runs. You’ll want to provide fully lowercased values (for example oauthserver.agarcial…, konnectivity.agarcial…) and either supply a valid hostname for Ignition or drop the field entirely so Hypershift can infer one. (kubernetes.io)

🤖 Prompt for AI Agents
In test/e2e/v2/tests/assets/hostedcluster-base.yaml around lines 29 to 49, the
Route hostnames for OAuthServer and Konnectivity use capital letters and
Ignition's hostname is empty, violating DNS-1123 rules; update the OAuthServer
and Konnectivity route hostnames to lowercase (e.g.
oauthserver.agarcial.hypershift.devcluster.openshift.com and
konnectivity.agarcial.hypershift.devcluster.openshift.com) and either supply a
valid non-empty, lowercased hostname for Ignition (e.g.
ignition.agarcial.hypershift.devcluster.openshift.com) or remove the empty
hostname field so Hypershift can infer it.

Comment on lines +147 to +195
for _, workload := range workloads {
It(fmt.Sprintf("should exist for pods with emptyDir or hostPath volumes belonging to %s", workload.Name), func() {
if shouldSkipWorkload(workload) {
Skip(fmt.Sprintf("workload %s is platform-specific and doesn't match cluster platform", workload.Name))
}

// Skip if workload is in exemption list
if slices.Contains(exemptions, workload.Name) {
Skip(fmt.Sprintf("workload %s is exempt from safe-to-evict annotations check", workload.Name))
}

pods := getWorkloadPods(workload)
if len(pods) == 0 {
Skip(fmt.Sprintf("no pods found for workload %s", workload.Name))
}

for _, pod := range pods {
// Check if pod has emptyDir or hostPath volumes
hasLocalVolumes := false
var localVolumeNames []string
for _, volume := range pod.Spec.Volumes {
if volume.EmptyDir != nil || volume.HostPath != nil {
hasLocalVolumes = true
localVolumeNames = append(localVolumeNames, volume.Name)
}
}

if hasLocalVolumes {
annotationKey := "cluster-autoscaler.kubernetes.io/safe-to-evict-local-volumes"
annotationValue, exists := pod.Annotations[annotationKey]
Expect(exists).To(BeTrue(), "pod %s has local volumes but missing safe-to-evict annotation", pod.Name)
Expect(annotationValue).NotTo(BeEmpty(), "pod %s has empty safe-to-evict annotation", pod.Name)

// Verify all local volumes are listed in annotation
annotatedVolumes := strings.Split(annotationValue, ",")
for _, volName := range localVolumeNames {
found := false
for _, annVol := range annotatedVolumes {
if strings.TrimSpace(annVol) == volName {
found = true
break
}
}
Expect(found).To(BeTrue(), "pod %s local volume %s not found in annotation", pod.Name, volName)
}
}
}
})
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Restore per-test workload capture in safe-to-evict checks.

This It closure reuses the outer loop variable, so every test ends up exercising only the final workload when the module isn’t built with Go ≥1.22 semantics. Rebind the variable inside the loop to keep the cases independent.

-		for _, workload := range workloads {
+		for _, workload := range workloads {
+			workload := workload // capture loop variable
 			It(fmt.Sprintf("should exist for pods with emptyDir or hostPath volumes belonging to %s", workload.Name), func() {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for _, workload := range workloads {
It(fmt.Sprintf("should exist for pods with emptyDir or hostPath volumes belonging to %s", workload.Name), func() {
if shouldSkipWorkload(workload) {
Skip(fmt.Sprintf("workload %s is platform-specific and doesn't match cluster platform", workload.Name))
}
// Skip if workload is in exemption list
if slices.Contains(exemptions, workload.Name) {
Skip(fmt.Sprintf("workload %s is exempt from safe-to-evict annotations check", workload.Name))
}
pods := getWorkloadPods(workload)
if len(pods) == 0 {
Skip(fmt.Sprintf("no pods found for workload %s", workload.Name))
}
for _, pod := range pods {
// Check if pod has emptyDir or hostPath volumes
hasLocalVolumes := false
var localVolumeNames []string
for _, volume := range pod.Spec.Volumes {
if volume.EmptyDir != nil || volume.HostPath != nil {
hasLocalVolumes = true
localVolumeNames = append(localVolumeNames, volume.Name)
}
}
if hasLocalVolumes {
annotationKey := "cluster-autoscaler.kubernetes.io/safe-to-evict-local-volumes"
annotationValue, exists := pod.Annotations[annotationKey]
Expect(exists).To(BeTrue(), "pod %s has local volumes but missing safe-to-evict annotation", pod.Name)
Expect(annotationValue).NotTo(BeEmpty(), "pod %s has empty safe-to-evict annotation", pod.Name)
// Verify all local volumes are listed in annotation
annotatedVolumes := strings.Split(annotationValue, ",")
for _, volName := range localVolumeNames {
found := false
for _, annVol := range annotatedVolumes {
if strings.TrimSpace(annVol) == volName {
found = true
break
}
}
Expect(found).To(BeTrue(), "pod %s local volume %s not found in annotation", pod.Name, volName)
}
}
}
})
}
for _, workload := range workloads {
workload := workload // capture loop variable
It(fmt.Sprintf("should exist for pods with emptyDir or hostPath volumes belonging to %s", workload.Name), func() {
if shouldSkipWorkload(workload) {
Skip(fmt.Sprintf("workload %s is platform-specific and doesn't match cluster platform", workload.Name))
}
// Skip if workload is in exemption list
if slices.Contains(exemptions, workload.Name) {
Skip(fmt.Sprintf("workload %s is exempt from safe-to-evict annotations check", workload.Name))
}
pods := getWorkloadPods(workload)
if len(pods) == 0 {
Skip(fmt.Sprintf("no pods found for workload %s", workload.Name))
}
for _, pod := range pods {
// Check if pod has emptyDir or hostPath volumes
hasLocalVolumes := false
var localVolumeNames []string
for _, volume := range pod.Spec.Volumes {
if volume.EmptyDir != nil || volume.HostPath != nil {
hasLocalVolumes = true
localVolumeNames = append(localVolumeNames, volume.Name)
}
}
if hasLocalVolumes {
annotationKey := "cluster-autoscaler.kubernetes.io/safe-to-evict-local-volumes"
annotationValue, exists := pod.Annotations[annotationKey]
Expect(exists).To(BeTrue(), "pod %s has local volumes but missing safe-to-evict annotation", pod.Name)
Expect(annotationValue).NotTo(BeEmpty(), "pod %s has empty safe-to-evict annotation", pod.Name)
// Verify all local volumes are listed in annotation
annotatedVolumes := strings.Split(annotationValue, ",")
for _, volName := range localVolumeNames {
found := false
for _, annVol := range annotatedVolumes {
if strings.TrimSpace(annVol) == volName {
found = true
break
}
}
Expect(found).To(BeTrue(), "pod %s local volume %s not found in annotation", pod.Name, volName)
}
}
}
})
}
🤖 Prompt for AI Agents
In test/e2e/v2/tests/control_plane_workloads_test.go around lines 147 to 195,
the It closure captures the loop variable workload causing all tests to
reference the final workload; rebind the variable before creating the closure
(e.g., w := workload) and use w in the It description and inside the test body
(getWorkloadPods(w), w.Name, etc.) so each test captures its own workload value.

Comment on lines +664 to +683

hasColocationAffinity := false
for _, term := range pod.Spec.Affinity.PodAffinity.PreferredDuringSchedulingIgnoredDuringExecution {
if term.Weight == 100 {
for _, req := range term.PodAffinityTerm.LabelSelector.MatchLabels {
if req == testCtx.ControlPlaneNamespace {
hasColocationAffinity = true
break
}
}
if term.PodAffinityTerm.LabelSelector != nil {
if term.PodAffinityTerm.LabelSelector.MatchLabels[colocationLabelKey] == testCtx.ControlPlaneNamespace {
hasColocationAffinity = true
break
}
}
}
}
Expect(hasColocationAffinity).To(BeTrue(), "pod %s should have colocation pod affinity", pod.Name)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Guard PodAffinity label selectors before dereferencing.

Several control-plane pods omit PodAffinityTerm.LabelSelector; this loop dereferences it unconditionally and will panic the test binary. Gate the checks behind a nil guard so we fail via Expect instead of crashing.

-					hasColocationAffinity := false
-					for _, term := range pod.Spec.Affinity.PodAffinity.PreferredDuringSchedulingIgnoredDuringExecution {
-						if term.Weight == 100 {
-							for _, req := range term.PodAffinityTerm.LabelSelector.MatchLabels {
-								if req == testCtx.ControlPlaneNamespace {
-									hasColocationAffinity = true
-									break
-								}
-							}
-							if term.PodAffinityTerm.LabelSelector != nil {
-								if term.PodAffinityTerm.LabelSelector.MatchLabels[colocationLabelKey] == testCtx.ControlPlaneNamespace {
-									hasColocationAffinity = true
-									break
-								}
-							}
-						}
-					}
+					hasColocationAffinity := false
+					for _, term := range pod.Spec.Affinity.PodAffinity.PreferredDuringSchedulingIgnoredDuringExecution {
+						if term.Weight != 100 || term.PodAffinityTerm.LabelSelector == nil {
+							continue
+						}
+						for _, value := range term.PodAffinityTerm.LabelSelector.MatchLabels {
+							if value == testCtx.ControlPlaneNamespace {
+								hasColocationAffinity = true
+								break
+							}
+						}
+						if term.PodAffinityTerm.LabelSelector.MatchLabels[colocationLabelKey] == testCtx.ControlPlaneNamespace {
+							hasColocationAffinity = true
+							break
+						}
+					}
🤖 Prompt for AI Agents
In test/e2e/v2/tests/control_plane_workloads_test.go around lines 664 to 683,
the loop dereferences term.PodAffinityTerm.LabelSelector without checking for
nil which can panic when LabelSelector is absent; update the loop to first check
if term.PodAffinityTerm != nil and term.PodAffinityTerm.LabelSelector != nil
before accessing MatchLabels, and only then inspect MatchLabels and the
colocationLabelKey; leave the Expect(hasColocationAffinity) assertion as-is so
pods missing the selector will cause a test failure rather than a runtime panic.

@csrwng
csrwng marked this pull request as ready for review November 7, 2025 01:45
@openshift-ci openshift-ci Bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Nov 7, 2025
@openshift-ci
openshift-ci Bot requested review from jparrill and sjenning November 7, 2025 01:45
@csrwng

csrwng commented Nov 7, 2025

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Nov 7, 2025

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (6)
test/e2e/util/crd.go (1)

48-75: Cover array items during schema traversal.
Line 60 still skips schema.Items, so any path that passes through a list (e.g. status.conditions.type) is always reported missing. That breaks consumers of HasFieldInCRDSchema. Please descend into Items.Schema and Items.JSONSchemas without advancing the path index before falling back to the combinators. Example fix:

@@
-	// Check AllOf, AnyOf, OneOf - these can contain the field
+	// Bridge into array items before other combinators
+	if schema.Items != nil {
+		if schema.Items.Schema != nil && hasFieldInSchema(schema.Items.Schema, pathParts, index) {
+			return true
+		}
+		for i := range schema.Items.JSONSchemas {
+			if hasFieldInSchema(&schema.Items.JSONSchemas[i], pathParts, index) {
+				return true
+			}
+		}
+	}
+
+	// Check AllOf, AnyOf, OneOf - these can contain the field
test/e2e/v2/tests/assets/hostedcluster-base.yaml (1)

35-49: Fix invalid Route hostnames.
Lines 39-49 still violate DNS-1123: the OAuthServer/Konnectivity hostnames contain uppercase letters and Ignition leaves hostname empty, so the manifest is rejected before tests run. Lowercase the hostnames and supply (or omit) Ignition’s hostname. For example:

@@
-        hostname: OAuthServer.agarcial.hypershift.devcluster.openshift.com
+        hostname: oauthserver.agarcial.hypershift.devcluster.openshift.com
@@
-        hostname: Konnectivity.agarcial.hypershift.devcluster.openshift.com
+        hostname: konnectivity.agarcial.hypershift.devcluster.openshift.com
@@
-      route:
-        
+      route:
+        hostname: ignition.agarcial.hypershift.devcluster.openshift.com
test/e2e/util/version.go (1)

64-78: Guard against nil HostedCluster version status.
Line 65 still assumes hostedCluster.Status.Version exists. When status hasn’t populated yet, this dereference panics before you can log the warning. Add nil checks before touching History, e.g.:

 func SetReleaseVersionFromHostedCluster(ctx context.Context, hostedCluster *hyperv1.HostedCluster) error {
-	if len(hostedCluster.Status.Version.History) == 0 || hostedCluster.Status.Version.History[0].Version == "" {
+	if hostedCluster.Status.Version == nil {
+		ginkgo.GinkgoWriter.Write([]byte("WARNING: hostedCluster status has no version information"))
+		return nil
+	}
+	if len(hostedCluster.Status.Version.History) == 0 || hostedCluster.Status.Version.History[0].Version == "" {
 		ginkgo.GinkgoWriter.Write([]byte("WARNING: cannot determine release version from HostedCluster"))
 		return nil
 	}
test/e2e/v2/tests/api_ux_validation_test.go (1)

1303-1316: Fix the empty-image mutation

This “image is empty” case still sets the release image to "@", so it duplicates the bad-format test and never exercises the empty-string validation path. Set the mutation to "" (and update the expectation to assert the empty-image error) so regressions in that branch are caught.

-			It("should reject when image is empty", func() {
+			It("should reject when image is empty", func() {
 				err := testNodePoolCreation(ctx, mgmtClient, "nodepool-base.yaml", func(np *hyperv1.NodePool) {
-					np.Spec.Release.Image = "@"
+					np.Spec.Release.Image = ""
 				})
 				Expect(err).To(HaveOccurred())
-				Expect(err.Error()).To(ContainSubstring("Image must start with a word character (letters, digits, or underscores) and contain no white spaces"))
+				Expect(err.Error()).To(ContainSubstring("Image must not be empty"))
 			})
test/e2e/v2/tests/control_plane_workloads_test.go (2)

79-186: Capture the workload per test registration

Each It closure still closes over the loop’s workload variable. On Go releases prior to 1.22—or modules that haven’t opted into the new semantics—all registered specs run against the final workload only. Rebind inside the loop (workload := workload) before declaring the It so every test captures its own case. Apply the same fix across the other workload loops.

-		for _, workload := range workloads {
+		for _, workload := range workloads {
+			workload := workload
 			if workload.Type != "Deployment" {
 				continue
 			}

680-698: Guard nil PodAffinity label selectors

Some control-plane pods omit PodAffinityTerm.LabelSelector; this loop dereferences it before checking, which panics the test binary. Bail out early when PodAffinityTerm or its LabelSelector is nil, and only then inspect MatchLabels.

-					for _, term := range pod.Spec.Affinity.PodAffinity.PreferredDuringSchedulingIgnoredDuringExecution {
-						if term.Weight == 100 {
-							for _, req := range term.PodAffinityTerm.LabelSelector.MatchLabels {
+					for _, term := range pod.Spec.Affinity.PodAffinity.PreferredDuringSchedulingIgnoredDuringExecution {
+						if term.Weight != 100 || term.PodAffinityTerm == nil || term.PodAffinityTerm.LabelSelector == nil {
+							continue
+						}
+						for _, value := range term.PodAffinityTerm.LabelSelector.MatchLabels {
-								if req == testCtx.ControlPlaneNamespace {
+							if value == testCtx.ControlPlaneNamespace {
 									hasColocationAffinity = true
 									break
 								}
 							}
-							if term.PodAffinityTerm.LabelSelector != nil {
-								if term.PodAffinityTerm.LabelSelector.MatchLabels[colocationLabelKey] == testCtx.ControlPlaneNamespace {
-									hasColocationAffinity = true
-									break
-								}
-							}
+						if term.PodAffinityTerm.LabelSelector.MatchLabels[colocationLabelKey] == testCtx.ControlPlaneNamespace {
+							hasColocationAffinity = true
+							break
+						}
 						}
 					}
🧹 Nitpick comments (1)
test/e2e/v2/tests/assets/nodepool-base.yaml (1)

4-5: Clean up empty annotation and label fields.

Lines 4-5 define annotations: and labels: with no values, which creates implicit null entries. For clarity and to match Kubernetes conventions, either remove these lines or use explicit empty object syntax (annotations: {}).

Apply this diff:

  metadata:
-  annotations:
-  labels:
  name: base

Alternatively, if you prefer to keep the structure as a reminder for future expansion:

  metadata:
-  annotations:
-  labels:
+  annotations: {}
+  labels: {}
  name: base
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between d8333bd and f236820.

📒 Files selected for processing (13)
  • test/e2e/util/crd.go (1 hunks)
  • test/e2e/util/version.go (2 hunks)
  • test/e2e/v2/internal/env_vars.go (1 hunks)
  • test/e2e/v2/internal/test_context.go (1 hunks)
  • test/e2e/v2/internal/workload_registry.go (1 hunks)
  • test/e2e/v2/internal/workload_resolver.go (1 hunks)
  • test/e2e/v2/tests/api_ux_validation_test.go (1 hunks)
  • test/e2e/v2/tests/assets/hostedcluster-base.yaml (1 hunks)
  • test/e2e/v2/tests/assets/karpenter-nodepool.yaml (1 hunks)
  • test/e2e/v2/tests/assets/karpenter-workloads.yaml (1 hunks)
  • test/e2e/v2/tests/assets/nodepool-base.yaml (1 hunks)
  • test/e2e/v2/tests/control_plane_workloads_test.go (1 hunks)
  • test/e2e/v2/tests/suite_test.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • test/e2e/v2/internal/workload_resolver.go
  • test/e2e/v2/internal/env_vars.go
  • test/e2e/v2/tests/assets/karpenter-workloads.yaml
  • test/e2e/v2/tests/assets/karpenter-nodepool.yaml
🧰 Additional context used
📓 Path-based instructions (1)
**

⚙️ CodeRabbit configuration file

-Focus on major issues impacting performance, readability, maintainability and security. Avoid nitpicks and avoid verbosity.

Files:

  • test/e2e/util/crd.go
  • test/e2e/v2/tests/api_ux_validation_test.go
  • test/e2e/v2/tests/suite_test.go
  • test/e2e/util/version.go
  • test/e2e/v2/internal/test_context.go
  • test/e2e/v2/internal/workload_registry.go
  • test/e2e/v2/tests/control_plane_workloads_test.go
  • test/e2e/v2/tests/assets/nodepool-base.yaml
  • test/e2e/v2/tests/assets/hostedcluster-base.yaml

Introduce a new Ginkgo v2 based end-to-end test suite with improved
structure and organization. The new suite includes:

- Suite setup with BeforeSuite/AfterSuite hooks for test initialization
- Test organization under test/e2e/v2/tests/ directory
- Internal utilities and helpers in test/e2e/v2/internal/
- CRD utility functions in test/e2e/util/ for cross-version compatibility

This new test framework will enable more maintainable and scalable e2e
testing with better parallel execution support and clearer test structure.

Signed-off-by: Cesar Wong <cewong@redhat.com>
Assisted-by: Claude 3.7 Sonnet (via Claude Code)
@csrwng

csrwng commented Nov 7, 2025

Copy link
Copy Markdown
Contributor Author

@coderabbitai Thank you for the thorough review! I've addressed all the identified issues:

1. Array traversal in CRD schema validation (test/e2e/util/crd.go:80)
✅ Added logic to traverse schema.Items without advancing path index, fixing false negatives for array fields like status.conditions.type.

2. Nil pointer guards in version utility (test/e2e/util/version.go:78)
✅ Added nil checks for hostedCluster.Status.Version while preserving original warning behavior for empty versions.

3. CronJob workload resolution (test/e2e/v2/internal/workload_resolver.go:199)
✅ Removed Job from early break condition to allow Job→CronJob traversal. Standalone Jobs still work correctly since workload matching happens before the break condition.

4. Invalid DNS hostnames (test/e2e/v2/tests/assets/hostedcluster-base.yaml:49)
✅ Fixed uppercase hostnames and added missing Ignition hostname to comply with DNS-1123 rules.

5. Loop variable capture (test/e2e/v2/tests/control_plane_workloads_test.go:184)
⏭️ Skipped - Not needed in Go 1.24+ where loop variable capture is handled automatically.

6. Nil guards for pod affinity checks (test/e2e/v2/tests/control_plane_workloads_test.go:698)
✅ Added nil check for term.PodAffinityTerm.LabelSelector to prevent panics when LabelSelector is missing.

7. Duplicate test case (test/e2e/v2/tests/api_ux_validation_test.go:1317)
✅ Fixed empty image test to actually test empty string instead of duplicating the bad format case.

All changes maintain backward compatibility while improving code robustness and test accuracy.


AI-assisted response via Claude Code

@coderabbitai

coderabbitai Bot commented Nov 7, 2025

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@csrwng Thank you for the detailed update! I really appreciate the thoroughness of your fixes and the clear summary of how each issue was addressed.

Your changes look solid:

  • The array traversal fix for CRD schema validation is exactly what was needed
  • The nil guards add important defensive checks
  • The CronJob workload resolution fix properly handles the traversal while preserving standalone Job functionality
  • The DNS hostname compliance fixes ensure proper validation
  • Smart call on skipping the loop variable capture issue for Go 1.24+
  • The pod affinity nil check prevents potential runtime panics
  • Good catch fixing the duplicate test case

The fixes enhance the robustness of the test suite while maintaining backward compatibility. Great work on getting this comprehensive v2 test suite ready! 🎉

@csrwng csrwng changed the title test(e2e-v2): add ginkgo-based v2 test suite NO-JIRA: test(e2e-v2): add ginkgo-based v2 test suite Nov 7, 2025
@openshift-ci-robot openshift-ci-robot added the jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. label Nov 7, 2025
@openshift-ci-robot

Copy link
Copy Markdown

@csrwng: This pull request explicitly references no jira issue.

Details

In response to this:

Introduce a new Ginkgo v2 based end-to-end test suite with improved
structure and organization. The new suite includes:

  • Suite setup with BeforeSuite/AfterSuite hooks for test initialization
  • Test organization under test/e2e/v2/tests/ directory
  • Internal utilities and helpers in test/e2e/v2/internal/
  • CRD utility functions in test/e2e/util/ for cross-version compatibility

This new test framework will enable more maintainable and scalable e2e
testing with better parallel execution support and clearer test structure.

Signed-off-by: Cesar Wong cewong@redhat.com
Assisted-by: Claude 3.7 Sonnet (via Claude Code)

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@csrwng csrwng added the lgtm Indicates that a PR is ready to be merged. label Nov 7, 2025
@openshift-merge-bot
openshift-merge-bot Bot merged commit 6644889 into openshift:feat-e2e-v2 Nov 7, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files. area/testing Indicates the PR includes changes for e2e testing jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. lgtm Indicates that a PR is ready to be merged.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants